/* Kernel module to addrange parameters. */

/* (C) 2013 LeadSec
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/semaphore.h>
#include <linux/list.h>
#include <linux/net.h>
#include <linux/string.h>
#include <linux/errno.h>
#include <linux/uaccess.h>
#include <linux/ip.h>
#include <linux/ipv6.h>
#include <linux/netfilter.h>
#include <linux/netfilter/x_tables.h>

#include "class_core.h"
#include "addrobj.h"

MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "LeadSec" );
MODULE_DESCRIPTION( "addrange object kernel module" );

static unsigned long
addrange_find_obj( const char *obj_name, unsigned char flags )
{
	return ( unsigned long )addr_range_find_obj( obj_name, flags );
}

static void
addrange_bind_obj( unsigned long addr )
{
	return addr_range_bind_obj( addr );
}

static void
addrange_release_obj( unsigned long obj_addr )
{
	return addr_range_release_obj( obj_addr );
}

static int
addrange_match_func( unsigned long obj_addr, int cmd, void *info, int *len )
{
	int ret = 0;

	switch ( cmd ) {
	case MATCH_PKT_IPV4:
		ret =  addr_range_matchv4_func( obj_addr, (const u32*)info ) ;
		break;
	case MATCH_PKT_IPV6:
		ret =  addr_range_matchv6_func( obj_addr, (const struct in6_addr *)info ) ;
	default:
		duprintf( "Unkonw command about service object!\n" );
		ret = 0;
		break;
	}
	return ret;
}

class_item_t addr_range_class  = {
	.class_name	= "addrobj",
	.bind_class	= &bind_class_item,
	.release_class = &release_class_item,
	.find_object	= &addrange_find_obj,
	.bind_object	= &addrange_bind_obj,
	.release_object	= &addrange_release_obj,
	.do_stuff	= &addrange_match_func,
	.refcnt		= ATOMIC_INIT( 0 ),
	/* addrange object configure */
	.cfg = {
		.max_size = 8096,
		.cur_size	= ATOMIC_INIT( 0 ),
		.max_number = 4096,
	},
};
EXPORT_SYMBOL( addr_range_class );

static int
do_add_addrange_cmd( void *user, unsigned int len )
{
	addr_range_request_t req;
	int ret = 0;

	if ( len != sizeof(addr_range_request_t) ) {
		duprintf( "addr_range object infomation len is invalid!\n" );
		return -ENOPROTOOPT;
	}
	
	ret = copy_from_user( &req, user, len );
	if ( ret != 0 ) {
		duprintf( "addr_range copy from userspace to kernel failed.!\n" );
		return ret;
	}

	ret = addr_range_construct_item( &req );
	if( ret != 0 ) {
		duprintf( "Construct addrange_item failed.\n" );
		return ret;
	}
	

	return ret;
}
	
static int
do_delete_addrange_cmd( void *user, int len )
{
	addr_range_request_t req; 
	int ret = 0;

	if ( len != sizeof(addr_range_request_t) ) {
		duprintf( "service object infomation len is invalid!\n" );
		return -ENOPROTOOPT;
	}

	ret = copy_from_user( &req, user, len );
	if ( ret != 0 ) {
		duprintf( "Servobj copy from userspace to kernel failed.!\n" );
		return ret;
	}

	ret = addr_range_destruct_item( &req );
	if ( ret != 0 ) {
		duprintf( "Destruct addrange_item failed.\n" );
		return ret;
	}

	return ret;
}

static int
do_add_addrcell_cmd( void *user, int len )
{
	addr_range_request_t req;
	int ret = 0;

	if ( len != sizeof(addr_range_request_t) ) {
		duprintf( "addr object infomation len is invalid!\n" );
		return -ENOPROTOOPT;
	}

	ret = copy_from_user( &req, user, len );
	if ( ret != 0 ) {
		duprintf( "addr object copy from userspace to kernel failed.!\n" );
		return ret;
	}

	ret = addr_range_construct_cell( &req );
	if ( 0 != ret ) {
		duprintf( "Construct cell failed!\n" );
		return ret;
	}
	
	return ret;
}

static int 
do_delete_addrcell_cmd( void *user, int len )
{
	addr_range_request_t req;
	int ret = 0;
	
	if ( len != sizeof(addr_range_request_t) ) {
		duprintf( "addr object infomation len is invalid!\n" );
		return -ENOPROTOOPT;
	}

	ret = copy_from_user( &req, user, len );
	if ( ret != 0 ) {
		duprintf( "delete object copy from userspace to kernel failed.!\n" );
		return ret;
	}

	ret = addr_range_destruct_cell( &req );
	if ( 0 != ret ) {
		duprintf( "Destruct addrobj cell failed!\n" );
		return ret;
	}
	return ret;
}

static int
do_flush_addrange_cmd( void *user, int len )
{
	addr_range_request_t req;
	int ret = 0;

	if ( len != sizeof(addr_range_request_t) ) {
		duprintf( "addr object infomation len is invalid!\n" );
		return -ENOPROTOOPT;
	}

	ret = copy_from_user( &req, user, len );
	if ( ret != 0 ) {
		duprintf( "flush object copy from userspace to kernel failed.!\n" );
		return ret;
	}

	ret = addr_range_flush_item( &req );
	if ( ret != 0 ) {
		duprintf( "Flush all addrange failed!\n" );
		return ret;
	}

	return ret;
}

static int
do_exist_addrange_cmd( void *user, int *len )
{
	addr_range_request_t req;
	int ret = 0;
	
	if ( *len != sizeof(addr_range_request_t) ) {
		duprintf( "service object infomation len is invalid!\n" );
		return -ENOPROTOOPT;
	}

	ret = copy_from_user( &req, user, *len );
	if ( ret != 0 ) {
		duprintf( "Get serobj_name from userspace to kernel failed.\n" );
		return ret;
	}

	*len = addr_range_exist_item( &req );
	
	return ret;
}

static int
do_addrange_set_ctl( struct sock *sk, int cmd, void __user *user, unsigned int len )
{
	int ret = 0;

	if ( (u32) cmd < ADDR_RANGE_OBJ_BASE || 
		(u32)cmd > ADDR_RANGE_OBJ_SET_MAX )	
		return -EINVAL;
	
	switch ( cmd ) {
	case ADDR_RANGE_OBJ_ADD:
		ret = do_add_addrange_cmd( user, len );
		break;

	case ADDR_RANGE_OBJ_DELETE:
		ret = do_delete_addrange_cmd( user, len );
		break;

	case ADDR_RANGE_OBJ_FLUSH:
		ret = do_flush_addrange_cmd( user, len );
		break;
		
	case ADDR_RANGE_CELL_ADD:
		ret = do_add_addrcell_cmd( user, len );
		break;
		
	case ADDR_RANGE_CELL_DELETE:
		ret = do_delete_addrcell_cmd( user, len );
		break;
				
	default:
		duprintf( "No such command for addrange!\n" );
		break;
	}

	return ret;
}

static int
do_addrange_get_ctl( struct sock *sk, int cmd, void __user *user, int *len )
{
	int ret = 0;

	if ( (u32)cmd > ADDR_RANGE_OBJ_GET_MAX ||
		(u32)cmd < ADDR_RANGE_OBJ_BASE )	
		return -EINVAL;

	switch ( cmd ) {
	case ADDR_RANGE_OBJ_EXIST:
		ret = do_exist_addrange_cmd( user, len );
		break;

	default:
		break;
	}

	return ret;
}

static struct nf_sockopt_ops addr_rangeobj_sockopts = {
	.pf		= PF_INET,
	.set_optmin	= ADDR_RANGE_OBJ_BASE,
	.set_optmax	= ADDR_RANGE_OBJ_SET_MAX + 1,
	.set		= do_addrange_set_ctl,
	
	.get_optmin	= ADDR_RANGE_OBJ_BASE,
	.get_optmax	= ADDR_RANGE_OBJ_GET_MAX + 1,
	.get		= do_addrange_get_ctl,
	.owner		= THIS_MODULE,

};

/* Match function */
static bool 
addrange_mt4( const struct sk_buff *skb, struct xt_action_param *par )
{
	class_match_t *info = ( class_match_t * )par->matchinfo;
	const struct iphdr *iph = ip_hdr(skb);
	int ret;

	if ( info->flags & ADDROBJ_SRC ) {
		ret = info->class_ptr->do_stuff( info->obj_addr, 
		MATCH_PKT_IPV4, (void*)&iph->saddr, NULL );
		if ( !ret )
			return ret;
	}
	if ( info->flags & ADDROBJ_DST ) {
		ret = info->class_ptr->do_stuff( info->obj_addr, 
		MATCH_PKT_IPV4, (void*)&iph->daddr, NULL );
		if ( !ret )
			return ret;
	}

	return true;
}

static bool 
addrange_mt6( const struct sk_buff *skb, struct xt_action_param *par )
{
	class_match_t *info = ( class_match_t * )par->matchinfo;
	const struct ipv6hdr *iph = ipv6_hdr(skb);
	int ret;
	
	if ( info->flags & ADDROBJ_SRC ) {
		ret = info->class_ptr->do_stuff( info->obj_addr, 
		MATCH_PKT_IPV6, (void*)&iph->saddr, NULL );
		if ( !ret )
			return ret;
	}
	if ( info->flags & ADDROBJ_DST ) {
		ret = info->class_ptr->do_stuff( info->obj_addr, 
		MATCH_PKT_IPV6, (void*)&iph->daddr, NULL );
		if ( !ret )
			return ret;
	}

	return true;
}

static int
addrange_mt_check( const struct xt_mtchk_param *par )
{	
	return ipt_class_checkentry( par->matchinfo );
}

static void
addrange_mt_destroy( const struct xt_mtdtor_param *par )
{
	return ipt_class_destroy( par->matchinfo );
}

static struct xt_match addr_range_match[] __read_mostly = {
	{
		.name		= "srcaddr",
		.family		= NFPROTO_IPV4,
		.match		= addrange_mt4,
		.checkentry	= addrange_mt_check,
		.destroy	= addrange_mt_destroy,
		.matchsize	= sizeof( class_match_t ),
		.me		= THIS_MODULE,
	},
	{
		.name		= "srcaddr",
		.family		= NFPROTO_IPV6,
		.match		= addrange_mt6,
		.checkentry 	= addrange_mt_check,
		.destroy	= addrange_mt_destroy,
		.matchsize	= sizeof( class_match_t ),
		.me		= THIS_MODULE,
	},
	{
		.name		= "dstaddr",
		.family		= NFPROTO_IPV4,
		.match		= addrange_mt4,
		.checkentry	= addrange_mt_check,
		.destroy	= addrange_mt_destroy,
		.matchsize	= sizeof( class_match_t ),
		.me		= THIS_MODULE,
	},
	{
		.name		= "dstaddr",
		.family		= NFPROTO_IPV6,
		.match		= addrange_mt6,
		.checkentry 	= addrange_mt_check,
		.destroy	= addrange_mt_destroy,
		.matchsize	= sizeof( class_match_t ),
		.me		= THIS_MODULE,
	},

};

static int __init
addrange_init( void )
{
	int ret = 0;
	/* Register class_item */
	ret = register_class_item( &addr_range_class );
	if ( ret < 0 ) {
		duprintf( "addrange_init: Register class item [addrange] failed!\n" );
		return ret;
	}
	
	/* Register socket_opt */
	ret = nf_register_sockopt( &addr_rangeobj_sockopts );
	if ( ret < 0 ) {
		duprintf( "addrange_init: Register sockopt failed!\n" );
		goto unreg_class;
	}

	/* Register match */
	ret = xt_register_matches( addr_range_match, ARRAY_SIZE(addr_range_match) );
	if ( ret < 0 ) {
		duprintf( "addrange_init: Register addrange match failed!\n" );
		goto unreg_sockopt;
	}

	ret = addr_range_list_init();
	if ( 0 != ret ) {
		duprintf( "Initiation addr_range_head array failed!\n" );
		goto unregister_match;
	}

	ret = addrange_obj_proc_init();
	if ( 0 != ret ) {
		duprintf( "Register proc file system failed.\n" );
		goto addr_list_free;
	}
	
	return ret;

addr_list_free:
	addr_range_list_free();
	
unregister_match:
	xt_unregister_matches( addr_range_match, ARRAY_SIZE(addr_range_match) );

unreg_sockopt:
	nf_unregister_sockopt( &addr_rangeobj_sockopts );

unreg_class:
	unregister_class_item( &addr_range_class );
	
	return ret;
}

static void __exit
addrange_fint( void )
{
	/* unresigter match */
	xt_unregister_matches( addr_range_match, ARRAY_SIZE(addr_range_match) );
	/* unresigter socket_opt */
	nf_unregister_sockopt( &addr_rangeobj_sockopts );
	/* Flush all addrange_item */
	addr_range_clean_resource();
	/* Resigter class_item */
	unregister_class_item( &addr_range_class );
	addr_range_list_free();
	addr_range_clean_resource();
	addrange_obj_proc_fint();
}

module_init( addrange_init );
module_exit( addrange_fint );

